diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 19:03:21 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-28 19:03:21 +0000 |
| commit | 5036cf2908792cef45f06256e71f10920f647f49 (patch) | |
| tree | 3116e7419e872d45025d1d48e6ddaffe2ba2dd38 /app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx | |
| parent | 7ae037e9c2fc0be1fe68cecb461c5e1e837cb0da (diff) | |
(김준회) 기술영업 조선 RFQ (SHI/벤더)
Diffstat (limited to 'app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx')
| -rw-r--r-- | app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx new file mode 100644 index 00000000..5b0ffb61 --- /dev/null +++ b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx @@ -0,0 +1,219 @@ +// app/vendor/quotations/page.tsx +import * as React from "react"; +import Link from "next/link"; +import { Metadata } from "next"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { LogIn } from "lucide-react"; +import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { Shell } from "@/components/shell"; +import { getValidFilters } from "@/lib/data-table"; +import { type SearchParams } from "@/types/table"; +import { searchParamsVendorRfqCache } from "@/lib/techsales-rfq/validations"; +import { + TECH_SALES_QUOTATION_STATUSES, + TECH_SALES_QUOTATION_STATUS_CONFIG +} from "@/db/schema"; + +import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/techsales-rfq/service"; +import { VendorQuotationsTable } from "@/lib/techsales-rfq/vendor-response/table/vendor-quotations-table"; + +export const metadata: Metadata = { + title: "기술영업 견적서 관리", + description: "기술영업 RFQ 견적서를 관리합니다.", +}; + +interface VendorQuotationsPageProps { + searchParams: SearchParams; +} + +export default async function VendorQuotationsPage({ + searchParams, +}: VendorQuotationsPageProps) { + // 세션 확인 + const session = await getServerSession(authOptions); + + if (!session?.user) { + return ( + <Shell> + <div className="flex min-h-[400px] flex-col items-center justify-center space-y-4"> + <div className="text-center"> + <h2 className="text-2xl font-bold tracking-tight">로그인이 필요합니다</h2> + <p className="text-muted-foreground"> + 견적서를 확인하려면 로그인해주세요. + </p> + </div> + <Button asChild> + <Link href="/api/auth/signin"> + <LogIn className="mr-2 h-4 w-4" /> + 로그인 + </Link> + </Button> + </div> + </Shell> + ); + } + + // 벤더 ID 확인 (사용자의 회사 ID가 벤더 ID) + const vendorId = session.user.companyId; + if (!vendorId) { + return ( + <Shell> + <div className="flex min-h-[400px] flex-col items-center justify-center space-y-4"> + <div className="text-center"> + <h2 className="text-2xl font-bold tracking-tight">회사 정보가 없습니다</h2> + <p className="text-muted-foreground"> + 견적서를 확인하려면 회사 정보가 필요합니다. + </p> + </div> + </div> + </Shell> + ); + } + + // 검색 파라미터 파싱 및 검증 + const search = searchParamsVendorRfqCache.parse(searchParams); + const validFilters = getValidFilters(search.filters); + + // 견적서 상태별 개수 조회 + const statusCountsPromise = getQuotationStatusCounts(vendorId.toString()); + + // 견적서 목록 조회 + const quotationsPromise = getVendorQuotations( + { + flags: search.flags, + page: search.page, + perPage: search.perPage, + sort: search.sort, + filters: validFilters, + joinOperator: search.joinOperator, + search: search.search, + from: search.from, + to: search.to, + }, + vendorId.toString() + ); + + return ( + <Shell variant="fullscreen" className="h-full"> + {/* 고정 헤더 영역 */} + <div className="flex-shrink-0"> + <div className="flex-shrink-0 flex flex-col gap-4 md:flex-row md:items-center md:justify-between"> + <div> + <h1 className="text-3xl font-bold tracking-tight">기술영업 견적서</h1> + <p className="text-muted-foreground"> + 할당받은 RFQ에 대한 견적서를 작성하고 관리합니다. + </p> + </div> + </div> + + {/* 상태별 개수 카드 */} + <div className="flex-shrink-0"> + <React.Suspense + fallback={ + <div className="w-full overflow-x-auto"> + <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 min-w-fit"> + {Array.from({ length: 5 }).map((_, i) => ( + <Card key={i} className="min-w-[160px]"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium truncate">로딩중...</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold">-</div> + </CardContent> + </Card> + ))} + </div> + </div> + } + > + <StatusCards statusCountsPromise={statusCountsPromise} /> + </React.Suspense> + </div> + + {/* 견적서 테이블 */} + <div className="flex-1 min-h-0 overflow-hidden"> + <React.Suspense + fallback={ + <DataTableSkeleton + columnCount={12} + searchableColumnCount={2} + filterableColumnCount={3} + cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem"]} + shrinkZero + /> + } + > + <div className="h-full overflow-auto"> + <VendorQuotationsTable promises={Promise.all([quotationsPromise.then(result => ({ data: result.data, pageCount: result.pageCount }))])} /> + </div> + </React.Suspense> + </div> + </div> + </Shell> + ); +} + +// 상태별 개수 카드 컴포넌트 +async function StatusCards({ + statusCountsPromise, +}: { + statusCountsPromise: Promise<{ + data: { status: string; count: number }[] | null; + error: string | null; + }>; +}) { + const { data: statusCounts, error } = await statusCountsPromise; + + if (error || !statusCounts) { + return ( + <div className="w-full overflow-x-auto"> + <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 min-w-fit"> + <Card className="min-w-[160px]"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium truncate">오류</CardTitle> + </CardHeader> + <CardContent> + <div className="text-2xl font-bold text-red-600">-</div> + <p className="text-xs text-muted-foreground truncate"> + 데이터를 불러올 수 없습니다 + </p> + </CardContent> + </Card> + </div> + </div> + ); + } + + // 중앙화된 상태 설정 사용 + const statusEntries = Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({ + key: statusValue, + ...TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue] + })); + + console.log(statusCounts, "statusCounts") + + return ( + <div className="w-full overflow-x-auto"> + <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 min-w-fit"> + {statusEntries.map((status) => ( + <Card key={status.key} className="min-w-[160px]"> + <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> + <CardTitle className="text-sm font-medium truncate">{status.label}</CardTitle> + </CardHeader> + <CardContent> + <div className={`text-2xl font-bold ${status.color}`}> + {statusCounts.find(item => item.status === status.key)?.count || 0} + </div> + <p className="text-xs text-muted-foreground truncate"> + {status.description} + </p> + </CardContent> + </Card> + ))} + </div> + </div> + ); +}
\ No newline at end of file |
